/*
 * Copyright (c) 2009. Silenus Consultoria, S.L.
 */
package es.silenus.detecta.service.impl;

import es.silenus.detecta.dao.OrganizationDAO;
import es.silenus.detecta.dao.UserDAO;

import es.silenus.detecta.model.Organization;
import es.silenus.detecta.model.Role;
import es.silenus.detecta.model.User;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.ldap.core.DirContextOperations;

import org.springframework.security.GrantedAuthority;

import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import org.springframework.util.StringUtils;

import java.util.ArrayList;
import java.util.List;
import java.util.Locale;


/**
 * LDAP user mapper.
 *
 * @author <a href="mailto:malonso@silenus-consultoria.es">Mariano Alonso</a>
 *
 * @since 25-jun-2009 16:51:54
 */
public class OpenLDAPUserMapper implements LDAPUserMapper {
	/**
	 * Logger for this class.
	 */
	private static final Log LOG = LogFactory.getLog(OpenLDAPUserMapper.class);

	/**
	 * The object classes.
	 */
	private List<String> objectClasses;

	/**
	 * The user id attribute.
	 */
	private String userIdAttribute;

	/**
	 * The user name attribute.
	 */
	private String userNameAttribute;

	/**
	 * The user first name attribute.
	 */
	private String userFirstNameAttribute;

	/**
	 * The user last name attribute.
	 */
	private String userLastNameAttribute;

	/**
	 * The user display name attribute.
	 */
	private String userDisplayNameAttribute;

	/**
	 * The user given name attribute.
	 */
	private String userGivenNameAttribute;

	/**
	 * The user organization attribute.
	 */
	private String userOrganizationAttribute;

	/**
	 * The user password attribute.
	 */
	private String userPasswordAttribute;

	/**
	 * The user mail attribute.
	 */
	private String userMailAttribute;

	/**
	 * The user language attribute.
	 */
	private String userLanguageAttribute;

	/**
	 * The user role attribute.
	 */
	private String userRoleAttribute;

	/**
	 * The user gid attribute.
	 */
	private String userGidNumber;

	/**
	 * The user home directory.
	 */
	private String userHomeDirectory;

	/**
	 * The user DAO.
	 */
	private UserDAO userDAO;

	/**
	 * The organization DAO.
	 */
	private OrganizationDAO organizationDAO;

	/**
	 * The home prefix.
	 */
	private String homePrefix;

/**
   * Constructor.
   */
	public OpenLDAPUserMapper() {
		objectClasses = new ArrayList<String>();
		objectClasses.add("inetOrgPerson");
		objectClasses.add("posixAccount");
		objectClasses.add("top");

		userIdAttribute = "uidNumber";
		userNameAttribute = "uid";
		userFirstNameAttribute = "cn";
		userLastNameAttribute = "sn";
		userDisplayNameAttribute = "displayName";
		userGivenNameAttribute = "givenName";
		userOrganizationAttribute = "o";
		userPasswordAttribute = "userPassword";
		userMailAttribute = "mail";
		userLanguageAttribute = "preferredLanguage";
		userRoleAttribute = "employeeType";
		userGidNumber = "gidNumber";
		userHomeDirectory = "homeDirectory";

		homePrefix = "/home/";
	}

	/**
	 * Sets the object classes.
	 *
	 * @param objectClasses the object classes.
	 */
	public void setObjectClasses(final List<String> objectClasses) {
		this.objectClasses = objectClasses;
	}

	/**
	 * Sets the user id attribute.
	 *
	 * @param userIdAttribute the user id attribute.
	 */
	public void setUserIdAttribute(final String userIdAttribute) {
		this.userIdAttribute = userIdAttribute;
	}

	/**
	 * Sets the user name attribute.
	 *
	 * @param userNameAttribute the user name attribute.
	 */
	public void setUserNameAttribute(final String userNameAttribute) {
		this.userNameAttribute = userNameAttribute;
	}

	/**
	 * Sets the user first name attribute.
	 *
	 * @param userFirstNameAttribute the user first name attribute.
	 */
	public void setUserFirstNameAttribute(final String userFirstNameAttribute) {
		this.userFirstNameAttribute = userFirstNameAttribute;
	}

	/**
	 * Sets the user last name attribute.
	 *
	 * @param userLastNameAttribute the user last name attribute.
	 */
	public void setUserLastNameAttribute(final String userLastNameAttribute) {
		this.userLastNameAttribute = userLastNameAttribute;
	}

	/**
	 * Sets the user display name attribute.
	 *
	 * @param userDisplayNameAttribute the user display name attribute.
	 */
	public void setUserDisplayNameAttribute(final String userDisplayNameAttribute) {
		this.userDisplayNameAttribute = userDisplayNameAttribute;
	}

	/**
	 * Sets the user given name attribute.
	 *
	 * @param userGivenNameAttribute the user given name attribute.
	 */
	public void setUserGivenNameAttribute(final String userGivenNameAttribute) {
		this.userGivenNameAttribute = userGivenNameAttribute;
	}

	/**
	 * Sets the user organization attribute.
	 *
	 * @param userOrganizationAttribute the user organization attribute.
	 */
	public void setUserOrganizationAttribute(final String userOrganizationAttribute) {
		this.userOrganizationAttribute = userOrganizationAttribute;
	}

	/**
	 * Sets the user password attribute.
	 *
	 * @param userPasswordAttribute the user password attribute.
	 */
	public void setUserPasswordAttribute(final String userPasswordAttribute) {
		this.userPasswordAttribute = userPasswordAttribute;
	}

	/**
	 * Sets the user mail attribute.
	 *
	 * @param userMailAttribute the user mail attribute.
	 */
	public void setUserMailAttribute(final String userMailAttribute) {
		this.userMailAttribute = userMailAttribute;
	}

	/**
	 * Sets the user language attribute.
	 *
	 * @param userLanguageAttribute the user language attribute.
	 */
	public void setUserLanguageAttribute(final String userLanguageAttribute) {
		this.userLanguageAttribute = userLanguageAttribute;
	}

	/**
	 * Sets the user role attribute.
	 *
	 * @param userRoleAttribute the user role attribute.
	 */
	public void setUserRoleAttribute(final String userRoleAttribute) {
		this.userRoleAttribute = userRoleAttribute;
	}

	/**
	 * Sets the user gid attribute.
	 *
	 * @param userGidNumber the user gid attribute.
	 */
	public void setUserGidNumber(final String userGidNumber) {
		this.userGidNumber = userGidNumber;
	}

	/**
	 * Sets the user home directory.
	 *
	 * @param userHomeDirectory the user home directory.
	 */
	public void setUserHomeDirectory(final String userHomeDirectory) {
		this.userHomeDirectory = userHomeDirectory;
	}

	/**
	 * Sets the user DAO.
	 *
	 * @param userDAO the user DAO.
	 */
	public void setUserDAO(final UserDAO userDAO) {
		this.userDAO = userDAO;
	}

	/**
	 * Sets the organization DAO.
	 *
	 * @param organizationDAO the organization DAO.
	 */
	public void setOrganizationDAO(final OrganizationDAO organizationDAO) {
		this.organizationDAO = organizationDAO;
	}

	/**
	 * Sets the home prefix.
	 *
	 * @param homePrefix the home prefix.
	 */
	public void setHomePrefix(final String homePrefix) {
		this.homePrefix = homePrefix;
	}

	/**
	 * Creates a fully populated User object for use by the security framework.
	 *
	 * @param ctx the context object which contains the user information.
	 * @param username the user's supplied login name.
	 * @param authorities the list of authorities which the user should be given.
	 *
	 * @return the user object.
	 */
	@Transactional(propagation = Propagation.REQUIRED)
	public User mapUserFromContext(DirContextOperations ctx, String username, GrantedAuthority[] authorities) {
		User user = userDAO.findByLogin(username);

		if(user == null) {
			user = new User();
			user.setLogin(username);
		}

		// Try to resolve organization
		Organization organization = resolveOrganization(ctx, username, authorities);

		user.setOrganization(organization);

		if(StringUtils.hasLength(userDisplayNameAttribute)) {
			user.setFullname(ctx.getStringAttribute(userDisplayNameAttribute));
		} else {
			StringBuffer fullname = new StringBuffer();

			if(StringUtils.hasLength(userFirstNameAttribute)) {
				fullname.append(ctx.getStringAttribute(userFirstNameAttribute));
				fullname.append(" ");
			}

			if(StringUtils.hasLength(userLastNameAttribute)) {
				fullname.append(ctx.getStringAttribute(userLastNameAttribute));
			}

			user.setFullname(fullname.toString());
		}

		if(StringUtils.hasLength(userPasswordAttribute)) {
			String userPassword = new String((byte[])ctx.getObjectAttribute(userPasswordAttribute));

			user.setPassword(userPassword);
		}

		if(StringUtils.hasLength(userMailAttribute)) {
			user.setEmail(ctx.getStringAttribute(userMailAttribute));
		}

		if(StringUtils.hasLength(userLanguageAttribute)) {
			user.setLocale(getLocale(ctx.getStringAttribute(userLanguageAttribute)));
		}

		if(StringUtils.hasLength(userRoleAttribute)) {
			Role role = Role.valueOf(ctx.getStringAttribute(userRoleAttribute));

			if(role != null) {
				user.setRole(role);
			}
		}

		if(user.getId() > 0) {
			userDAO.update(user);
		} else {
			userDAO.save(user);
		}

		return user;
	}

	/**
	 * Resolves the organization.
	 *
	 * @param ctx the context.
	 * @param username the username.
	 * @param authorities the authorities.
	 *
	 * @return the organization or null.
	 */
	@Transactional(propagation = Propagation.REQUIRED)
	private Organization resolveOrganization(
		DirContextOperations ctx, final String username, GrantedAuthority[] authorities) {
		Organization organization = null;

		if(authorities.length > 0) {
			if(authorities.length > 1) {
				StringBuffer buffer = new StringBuffer("Found several authorities for user \"").append(username)
																																											 .append(
						"\", only the first valid one will be used");

				LOG.warn(buffer.toString());
			}

			for(GrantedAuthority authority : authorities) {
				organization = organizationDAO.findByName(authority.getAuthority());

				if(organization != null) {
					break;
				}
			}
		}

		if((organization == null) && StringUtils.hasLength(userOrganizationAttribute)) {
			String organizationName = ctx.getStringAttribute(userOrganizationAttribute);

			if(StringUtils.hasLength(organizationName)) {
				organization = organizationDAO.findByName(organizationName);
			}
		}

		if((organization == null) && StringUtils.hasLength(userGidNumber)) {
			Integer organizationId = Integer.valueOf(ctx.getObjectAttribute(userGidNumber).toString());

			if(organizationId != null) {
				organization = organizationDAO.findById(organizationId);
			}
		}

		return organization;
	}

	/**
	 * Retrieves the locale.
	 *
	 * @param preferredLanguage the preferred language.
	 *
	 * @return the locale.
	 */
	private Locale getLocale(final String preferredLanguage) {
		Locale result = Locale.getDefault();

		if(StringUtils.hasLength(preferredLanguage)) {
			String[] parts = preferredLanguage.split("-");

			if(parts.length == 2) {
				result = new Locale(parts[0], parts[1]);
			}
		}

		return result;
	}

	/**
	 * Splits a fullname into first name and surname.
	 *
	 * @param fullname the fullname.
	 *
	 * @return the first name and surname pair.
	 */
	private String[] splitFullname(final String fullname) {
		int idx = fullname.indexOf(" ");

		if(idx > -1) {
			return new String[] { fullname.substring(0, idx), fullname.substring(idx + 1) };
		} else {
			return new String[] { fullname, null };
		}
	}

	/**
	 * Retrieves the prefered language.
	 *
	 * @param locale the locale.
	 *
	 * @return the prefered language.
	 */
	private String getPreferredLanguage(final Locale locale) {
		StringBuilder builder = new StringBuilder();

		if(locale != null) {
			builder.append(locale.getLanguage()).append("-").append(locale.getCountry());
		}

		return builder.toString();
	}

	/**
	 * Reverse of the above operation. Populates a context object from the supplied user object. Called when saving
	 * a user, for example.
	 *
	 * @param user the user.
	 * @param ctx the context.
	 */
	@Transactional(propagation = Propagation.SUPPORTS)
	public void mapUserToContext(User user, DirContextOperations ctx) {
		if(!ctx.isUpdateMode()) {
			ctx.setAttributeValues("objectClass", objectClasses.toArray());
		}
		if(StringUtils.hasLength(userIdAttribute)) {
			ctx.setAttributeValue(userIdAttribute, String.valueOf(user.getId()));
		}

		if(StringUtils.hasLength(userNameAttribute)) {
			ctx.setAttributeValue(userNameAttribute, user.getLogin());
		}

		final String fullname = user.getFullname().trim();
		String[] nameParts = splitFullname(fullname);

		if(StringUtils.hasLength(userFirstNameAttribute)) {
			ctx.setAttributeValue(userFirstNameAttribute, nameParts[0]);
		}

		if(StringUtils.hasLength(userLastNameAttribute) && (nameParts[1] != null)) {
			if(nameParts[1] != null) {
				ctx.setAttributeValue(userLastNameAttribute, nameParts[1]);
			} else {
				ctx.removeAttributeValue(userLastNameAttribute, ctx.getObjectAttribute(userLastNameAttribute));
			}
		}

		if(StringUtils.hasLength(userGivenNameAttribute)) {
			ctx.setAttributeValue(userGivenNameAttribute, nameParts[0]);
		}

		if(StringUtils.hasLength(userDisplayNameAttribute)) {
			ctx.setAttributeValue(userDisplayNameAttribute, fullname);
		}


		if(StringUtils.hasLength(userOrganizationAttribute)) {
			ctx.setAttributeValue(userOrganizationAttribute, user.getOrganization().getName());
		}

		if(StringUtils.hasLength(userGidNumber)) {
			ctx.setAttributeValue(userGidNumber, String.valueOf(user.getOrganization().getId()));
		}

		if(StringUtils.hasLength(userPasswordAttribute)) {
			ctx.setAttributeValue(userPasswordAttribute, user.getPassword());
		}

		if(StringUtils.hasLength(userMailAttribute)) {
			ctx.setAttributeValue(userMailAttribute, user.getEmail());
		}

		if(StringUtils.hasLength(userLanguageAttribute)) {
			ctx.setAttributeValue(userLanguageAttribute, getPreferredLanguage(user.getLocale()));
		}

		if(StringUtils.hasLength(userRoleAttribute)) {
			ctx.setAttributeValue(userRoleAttribute, user.getRole().name());
		}

		if(StringUtils.hasLength(userHomeDirectory)) {
			ctx.setAttributeValue(userHomeDirectory, homePrefix + user.getLogin());
		}
	}
}
